﻿// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using static System.String;

namespace CSharpSyntaxGenerator
{
    internal class SourceWriter : AbstractFileWriter
    {
        private SourceWriter(TextWriter writer, Tree tree)
            : base(writer, tree)
        {
        }

        public static void WriteMain(TextWriter writer, Tree tree) => new SourceWriter(writer, tree).WriteMain();

        public static void WriteInternal(TextWriter writer, Tree tree) => new SourceWriter(writer, tree).WriteInternal();

        public static void WriteSyntax(TextWriter writer, Tree tree) => new SourceWriter(writer, tree).WriteSyntax();

        private void WriteFileHeader()
        {
            WriteLine("// <auto-generated />");
            WriteLine();
            WriteLine("#nullable enable");
            WriteLine();
            WriteLine("using System;");
            WriteLine("using System.Collections.Generic;");
            WriteLine("using Microsoft.CodeAnalysis.Syntax.InternalSyntax;");
            WriteLine("using Roslyn.Utilities;");
            WriteLine();
        }

        private void WriteInternal()
        {
            WriteFileHeader();

            WriteLine("namespace Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax");
            OpenBlock();
            WriteLine();
            this.WriteGreenTypes();
            this.WriteGreenVisitors();
            this.WriteGreenRewriter();
            this.WriteContextualGreenFactories();
            this.WriteStaticGreenFactories();
            CloseBlock();
        }

        private void WriteSyntax()
        {
            WriteFileHeader();
            WriteLine("namespace Microsoft.CodeAnalysis.CSharp.Syntax");
            OpenBlock();
            WriteLine();
            this.WriteRedTypes();
            CloseBlock();
        }

        private void WriteMain()
        {
            WriteFileHeader();
            WriteLine("namespace Microsoft.CodeAnalysis.CSharp");
            OpenBlock();
            WriteLine("using Microsoft.CodeAnalysis.CSharp.Syntax;");
            this.WriteRedVisitors();
            this.WriteRedRewriter();
            this.WriteRedFactories();
            CloseBlock();
        }

        private void WriteGreenTypes()
        {
            var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();
            foreach (var node in nodes)
            {
                WriteLine();
                this.WriteGreenType(node);
            }
        }

        private void WriteGreenType(TreeType node)
        {
            WriteComment(node.TypeComment, "");

            if (node is AbstractNode)
            {
                var nd = (AbstractNode)node;
                WriteLine($"internal abstract partial class {node.Name} : {node.Base}");
                OpenBlock();

                // ctor with diagnostics and annotations
                WriteLine($"internal {node.Name}(SyntaxKind kind, DiagnosticInfo[] diagnostics, SyntaxAnnotation[] annotations)");
                WriteLine("  : base(kind, diagnostics, annotations)");
                OpenBlock();
                if (node.Name == "DirectiveTriviaSyntax")
                {
                    WriteLine("this.flags |= NodeFlags.ContainsDirectives;");
                }
                CloseBlock();
                WriteLine();
                // ctor without diagnostics and annotations
                WriteLine($"internal {node.Name}(SyntaxKind kind)");
                WriteLine("  : base(kind)");
                OpenBlock();
                if (node.Name == "DirectiveTriviaSyntax")
                {
                    WriteLine("this.flags |= NodeFlags.ContainsDirectives;");
                }
                CloseBlock();

                // object reader constructor
                WriteLine();
                WriteLine($"protected {node.Name}(ObjectReader reader)");
                WriteLine("  : base(reader)");
                OpenBlock();
                if (node.Name == "DirectiveTriviaSyntax")
                {
                    WriteLine("this.flags |= NodeFlags.ContainsDirectives;");
                }
                CloseBlock();

                var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
                var nodeFields = nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

                foreach (var field in nodeFields)
                {
                    if (IsNodeOrNodeList(field.Type))
                    {
                        WriteLine();
                        WriteComment(field.PropertyComment, "");

                        if (IsSeparatedNodeList(field.Type) ||
                            IsNodeList(field.Type))
                        {
                            WriteLine($"public abstract {(IsNew(field) ? "new " : "")}Microsoft.CodeAnalysis.Syntax.InternalSyntax.{field.Type} {field.Name} {{ get; }}");
                        }
                        else
                        {
                            WriteLine($"public abstract {(IsNew(field) ? "new " : "")}{(GetFieldType(field, green: true))} {field.Name} {{ get; }}");
                        }
                    }
                }

                foreach (var field in valueFields)
                {
                    WriteLine();
                    WriteComment(field.PropertyComment, "");

                    WriteLine($"public abstract {(IsNew(field) ? "new " : "")}{field.Type} {field.Name} {{ get; }}");
                }

                CloseBlock();
            }
            else if (node is Node)
            {
                var nd = (Node)node;

                WriteLine($"internal sealed partial class {node.Name} : {node.Base}");
                OpenBlock();

                var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
                var nodeFields = nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

                foreach (var field in nodeFields)
                {
                    var type = GetFieldType(field, green: true);
                    WriteLine($"internal readonly {type} {CamelCase(field.Name)};");
                }

                foreach (var field in valueFields)
                {
                    WriteLine($"internal readonly {field.Type} {CamelCase(field.Name)};");
                }

                // write constructor with diagnostics and annotations
                WriteLine();
                Write($"internal {node.Name}(SyntaxKind kind");

                WriteGreenNodeConstructorArgs(nodeFields, valueFields);

                WriteLine(", DiagnosticInfo[] diagnostics, SyntaxAnnotation[] annotations)");
                WriteLine("  : base(kind, diagnostics, annotations)");
                OpenBlock();
                WriteCtorBody(valueFields, nodeFields);
                CloseBlock();

                // write constructor with async
                WriteLine();
                Write($"internal {node.Name}(SyntaxKind kind");

                WriteGreenNodeConstructorArgs(nodeFields, valueFields);

                WriteLine(", SyntaxFactoryContext context)");
                WriteLine("  : base(kind)");
                OpenBlock();
                WriteLine("this.SetFactoryContext(context);");
                WriteCtorBody(valueFields, nodeFields);
                CloseBlock();

                // write constructor without diagnostics and annotations
                WriteLine();
                Write($"internal {node.Name}(SyntaxKind kind");

                WriteGreenNodeConstructorArgs(nodeFields, valueFields);

                WriteLine(")");
                WriteLine("  : base(kind)");
                OpenBlock();
                WriteCtorBody(valueFields, nodeFields);
                CloseBlock();
                WriteLine();

                // property accessors
                foreach (var field in nodeFields)
                {
                    WriteComment(field.PropertyComment, "");
                    if (IsNodeList(field.Type))
                    {
                        WriteLine($"public {OverrideOrNewModifier(field)}Microsoft.CodeAnalysis.Syntax.InternalSyntax.{field.Type} {field.Name} => new Microsoft.CodeAnalysis.Syntax.InternalSyntax.{field.Type}(this.{CamelCase(field.Name)});");
                    }
                    else if (IsSeparatedNodeList(field.Type))
                    {
                        WriteLine($"public {OverrideOrNewModifier(field)}Microsoft.CodeAnalysis.Syntax.InternalSyntax.{field.Type} {field.Name} => new Microsoft.CodeAnalysis.Syntax.InternalSyntax.{field.Type}(new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList<CSharpSyntaxNode>(this.{CamelCase(field.Name)}));");
                    }
                    else if (field.Type == "SyntaxNodeOrTokenList")
                    {
                        WriteLine($"public {OverrideOrNewModifier(field)}Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList<CSharpSyntaxNode> {field.Name} => new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList<CSharpSyntaxNode>(this.{CamelCase(field.Name)});");
                    }
                    else
                    {
                        WriteLine($"public {OverrideOrNewModifier(field)}{(GetFieldType(field, green: true))} {field.Name} => this.{CamelCase(field.Name)};");
                    }
                }

                foreach (var field in valueFields)
                {
                    WriteComment(field.PropertyComment, "");
                    WriteLine($"public {OverrideOrNewModifier(field)}{field.Type} {field.Name} => this.{CamelCase(field.Name)};");
                }

                // GetSlot
                WriteLine();
                Write("internal override GreenNode? GetSlot(int index)");

                if (nodeFields.Count == 0)
                {
                    WriteLine(" => null;");
                }
                else if (nodeFields.Count == 1)
                {
                    WriteLine();
                    Indent();
                    WriteLine($"=> index == 0 ? this.{CamelCase(nodeFields[0].Name)} : null;");
                    Unindent();
                }
                else
                {
                    WriteLine();
                    Indent();
                    WriteLine("=> index switch");
                    OpenBlock();
                    for (int i = 0, n = nodeFields.Count; i < n; i++)
                    {
                        var field = nodeFields[i];
                        WriteLine($"{i} => this.{CamelCase(field.Name)},");
                    }
                    WriteLine("_ => null,");
                    CloseBlock(";");
                    Unindent();
                }

                WriteLine();
                WriteLine($"internal override SyntaxNode CreateRed(SyntaxNode? parent, int position) => new CSharp.Syntax.{node.Name}(this, parent, position);");

                this.WriteGreenAcceptMethods(nd);
                this.WriteGreenUpdateMethod(nd);
                this.WriteSetDiagnostics(nd);
                this.WriteSetAnnotations(nd);

                this.WriteGreenSerialization(nd);
                CloseBlock();
            }
        }

        private void WriteGreenNodeConstructorArgs(List<Field> nodeFields, List<Field> valueFields)
        {
            foreach (var field in nodeFields)
            {
                Write($", {(GetFieldType(field, green: true))} {CamelCase(field.Name)}");
            }

            foreach (var field in valueFields)
            {
                Write($", {field.Type} {CamelCase(field.Name)}");
            }
        }

        private void WriteGreenSerialization(Node node)
        {
            var valueFields = node.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
            var nodeFields = node.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

            // object reader constructor
            WriteLine();
            WriteLine($"internal {node.Name}(ObjectReader reader)");
            WriteLine("  : base(reader)");
            OpenBlock();
            WriteLine($"this.SlotCount = {nodeFields.Count};");

            foreach (var field in nodeFields)
            {
                string type = GetFieldType(field, green: true);
                WriteLine($"var {CamelCase(field.Name)} = ({type})reader.ReadValue();");

                if (IsAnyList(field.Type) || IsOptional(field))
                {
                    WriteLine($"if ({CamelCase(field.Name)} != null)");
                    OpenBlock();
                    WriteLine($"AdjustFlagsAndWidth({CamelCase(field.Name)});");
                    WriteLine($"this.{CamelCase(field.Name)} = {CamelCase(field.Name)};");
                    CloseBlock();
                }
                else
                {
                    WriteLine($"AdjustFlagsAndWidth({CamelCase(field.Name)});");
                    WriteLine($"this.{CamelCase(field.Name)} = {CamelCase(field.Name)};");
                }
            }

            foreach (var field in valueFields)
            {
                WriteLine($"this.{CamelCase(field.Name)} = ({(GetFieldType(field, green: true))})reader.{(GetReaderMethod(GetFieldType(field, green: true)))}();");
            }

            CloseBlock();

            // IWritable
            WriteLine();
            WriteLine("internal override void WriteTo(ObjectWriter writer)");
            OpenBlock();
            WriteLine("base.WriteTo(writer);");

            foreach (var field in nodeFields)
            {
                WriteLine($"writer.WriteValue(this.{CamelCase(field.Name)});");
            }

            foreach (var field in valueFields)
            {
                var type = GetFieldType(field, green: true);
                WriteLine($"writer.{GetWriterMethod(type)}(this.{CamelCase(field.Name)});");
            }

            CloseBlock();

            // IReadable
            WriteLine();
            WriteLine($"static {node.Name}()");
            OpenBlock();
            WriteLine($"ObjectBinder.RegisterTypeReader(typeof({node.Name}), r => new {node.Name}(r));");
            CloseBlock();
        }

        private string GetWriterMethod(string type)
            => type switch
            {
                "bool" => "WriteBoolean",
                _ => throw new InvalidOperationException($"Type '{type}' not supported for object reader serialization."),
            };

        private string GetReaderMethod(string type)
            => type switch
            {
                "bool" => "ReadBoolean",
                _ => throw new InvalidOperationException($"Type '{type}' not supported for object reader serialization."),
            };

        private void WriteCtorBody(List<Field> valueFields, List<Field> nodeFields)
        {
            // constructor body
            WriteLine($"this.SlotCount = {nodeFields.Count};");

            foreach (var field in nodeFields)
            {
                if (IsAnyList(field.Type) || IsOptional(field))
                {
                    WriteLine($"if ({CamelCase(field.Name)} != null)");
                    OpenBlock();
                    WriteLine($"this.AdjustFlagsAndWidth({CamelCase(field.Name)});");
                    WriteLine($"this.{CamelCase(field.Name)} = {CamelCase(field.Name)};");
                    CloseBlock();
                }
                else
                {
                    WriteLine($"this.AdjustFlagsAndWidth({CamelCase(field.Name)});");
                    WriteLine($"this.{CamelCase(field.Name)} = {CamelCase(field.Name)};");
                }
            }

            foreach (var field in valueFields)
            {
                WriteLine($"this.{CamelCase(field.Name)} = {CamelCase(field.Name)};");
            }
        }

        private void WriteSetAnnotations(Node node)
        {
            WriteLine();
            WriteLine("internal override GreenNode SetAnnotations(SyntaxAnnotation[] annotations)");
            Write($"    => new {node.Name}(");
            Write(CommaJoin(
                "this.Kind",
                node.Fields.Select(f => $"this.{CamelCase(f.Name)}"),
                "GetDiagnostics()",
                "annotations"));
            WriteLine(");");
        }

        private void WriteSetDiagnostics(Node node)
        {
            WriteLine();
            WriteLine("internal override GreenNode SetDiagnostics(DiagnosticInfo[] diagnostics)");
            Write($"    => new {node.Name}(");
            Write(CommaJoin(
                "this.Kind",
                node.Fields.Select(f => $"this.{CamelCase(f.Name)}"),
                "diagnostics",
                "GetAnnotations()"));
            WriteLine(");");
        }

        private void WriteGreenAcceptMethods(Node node)
        {
            WriteLine();
            WriteLine($"public override void Accept(CSharpSyntaxVisitor visitor) => visitor.Visit{StripPost(node.Name, "Syntax")}(this);");
            WriteLine($"public override TResult Accept<TResult>(CSharpSyntaxVisitor<TResult> visitor) => visitor.Visit{StripPost(node.Name, "Syntax")}(this);");
        }

        private void WriteGreenVisitors()
        {
            WriteGreenVisitor(withResult: true);
            WriteGreenVisitor(withResult: false);
        }

        private void WriteGreenVisitor(bool withResult)
        {
            var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();

            WriteLine();
            WriteLine("internal partial class CSharpSyntaxVisitor" + (withResult ? "<TResult>" : ""));
            OpenBlock();
            foreach (var node in nodes.OfType<Node>())
            {
                WriteLine($"public virtual {(withResult ? "TResult" : "void")} Visit{StripPost(node.Name, "Syntax")}({node.Name} node) => this.DefaultVisit(node);");
            }
            CloseBlock();
        }

        private void WriteGreenUpdateMethod(Node node)
        {
            WriteLine();
            Write($"public {node.Name} Update(");
            Write(CommaJoin(node.Fields.Select(f =>
            {
                var type =
                    f.Type == "SyntaxNodeOrTokenList" ? "Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList<CSharpSyntaxNode>" :
                    f.Type == "SyntaxTokenList" ? "Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList<SyntaxToken>" :
                    IsNodeList(f.Type) ? "Microsoft.CodeAnalysis.Syntax.InternalSyntax." + f.Type :
                    IsSeparatedNodeList(f.Type) ? "Microsoft.CodeAnalysis.Syntax.InternalSyntax." + f.Type :
                    f.Type;

                return $"{type} {CamelCase(f.Name)}";
            })));
            WriteLine(")");
            OpenBlock();

            Write("if (");
            int nCompared = 0;
            foreach (var field in node.Fields)
            {
                if (IsDerivedOrListOfDerived("SyntaxNode", field.Type) || IsDerivedOrListOfDerived("SyntaxToken", field.Type) || field.Type == "SyntaxNodeOrTokenList")
                {
                    if (nCompared > 0)
                        Write(" || ");
                    Write($"{CamelCase(field.Name)} != this.{field.Name}");
                    nCompared++;
                }
            }
            if (nCompared > 0)
            {
                WriteLine(")");
                OpenBlock();
                Write($"var newNode = SyntaxFactory.{StripPost(node.Name, "Syntax")}(");
                Write(CommaJoin(
                    node.Kinds.Count > 1 ? "this.Kind" : "",
                    node.Fields.Select(f => CamelCase(f.Name))));
                WriteLine(");");
                WriteLine("var diags = GetDiagnostics();");
                WriteLine("if (diags?.Length > 0)");
                WriteLine("    newNode = newNode.WithDiagnosticsGreen(diags);");
                WriteLine("var annotations = GetAnnotations();");
                WriteLine("if (annotations?.Length > 0)");
                WriteLine("    newNode = newNode.WithAnnotationsGreen(annotations);");
                WriteLine("return newNode;");
                CloseBlock();
            }

            WriteLine();
            WriteLine("return this;");
            CloseBlock();
        }

        private void WriteGreenRewriter()
        {
            var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();

            WriteLine();
            WriteLine("internal partial class CSharpSyntaxRewriter : CSharpSyntaxVisitor<CSharpSyntaxNode>");
            OpenBlock();
            int nWritten = 0;
            foreach (var node in nodes.OfType<Node>())
            {
                var nodeFields = node.Fields.Where(nd => IsNodeOrNodeList(nd.Type)).ToList();

                if (nWritten > 0)
                    WriteLine();
                nWritten++;
                WriteLine($"public override CSharpSyntaxNode Visit{StripPost(node.Name, "Syntax")}({node.Name} node)");
                Indent();

                if (nodeFields.Count == 0)
                {
                    WriteLine("=> node;");
                }
                else
                {
                    Write("=> node.Update(");
                    Write(CommaJoin(node.Fields.Select(f =>
                    {
                        if (IsAnyList(f.Type))
                            return $"VisitList(node.{f.Name})";
                        else if (IsNode(f.Type))
                            return $"({f.Type})Visit(node.{f.Name})";
                        else
                            return $"node.{f.Name}";
                    })));
                    WriteLine(");");
                }

                Unindent();
            }

            CloseBlock();
        }

        private void WriteContextualGreenFactories()
        {
            var nodes = Tree.Types.Where(n => !(n is PredefinedNode) && !(n is AbstractNode)).ToList();
            WriteLine();
            WriteLine("internal partial class ContextAwareSyntax");
            OpenBlock();
            WriteLine();
            WriteLine("private SyntaxFactoryContext context;");

            WriteLine();
            WriteLine("public ContextAwareSyntax(SyntaxFactoryContext context)");
            WriteLine("    => this.context = context;");

            WriteGreenFactories(nodes, withSyntaxFactoryContext: true);
            CloseBlock();
        }

        private void WriteStaticGreenFactories()
        {
            var nodes = Tree.Types.Where(n => !(n is PredefinedNode) && !(n is AbstractNode)).ToList();
            WriteLine();
            WriteLine("internal static partial class SyntaxFactory");
            OpenBlock();
            WriteGreenFactories(nodes);
            WriteGreenTypeList();
            CloseBlock();
        }

        private void WriteGreenFactories(List<TreeType> nodes, bool withSyntaxFactoryContext = false)
        {
            foreach (var node in nodes.OfType<Node>())
            {
                WriteLine();
                this.WriteGreenFactory(node, withSyntaxFactoryContext);
            }
        }

        private void WriteGreenTypeList()
        {
            WriteLine();
            WriteLine("internal static IEnumerable<Type> GetNodeTypes()");
            Indent();
            WriteLine("=> new Type[]");
            OpenBlock();

            var nodes = Tree.Types.Where(n => !(n is PredefinedNode) && !(n is AbstractNode)).ToList();
            foreach (var node in nodes)
            {
                WriteLine($"typeof({node.Name}),");
            }

            CloseBlock(";");
            Unindent();
        }

        private void WriteGreenFactory(Node nd, bool withSyntaxFactoryContext = false)
        {
            var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
            var nodeFields = nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

            Write($"public {(withSyntaxFactoryContext ? "" : "static ")}{nd.Name} {StripPost(nd.Name, "Syntax")}(");
            WriteGreenFactoryParameters(nd);
            WriteLine(")");
            OpenBlock();

            // validate kind
            if (nd.Kinds.Count >= 2)
            {
                WriteLine("switch (kind)");
                OpenBlock();
                foreach (var kind in nd.Kinds)
                {
                    WriteLine($"case SyntaxKind.{kind.Name}:{(kind == nd.Kinds.Last() ? " break;" : "")}");
                }
                WriteLine("default: throw new ArgumentException(nameof(kind));");
                CloseBlock();
            }

            // validate parameters
            WriteLine("#if DEBUG");
            foreach (var field in nodeFields)
            {
                var pname = CamelCase(field.Name);

                if (!IsAnyList(field.Type) && !IsOptional(field))
                {
                    WriteLine($"if ({CamelCase(field.Name)} == null) throw new ArgumentNullException(nameof({CamelCase(field.Name)}));");
                }
                if (field.Type == "SyntaxToken" && field.Kinds != null && field.Kinds.Count > 0)
                {
                    if (IsOptional(field))
                    {
                        WriteLine($"if ({CamelCase(field.Name)} != null)");
                        OpenBlock();
                    }

                    if (field.Kinds.Count == 1 && !IsOptional(field))
                    {
                        WriteLine($"if ({pname}.Kind != SyntaxKind.{field.Kinds[0].Name}) throw new ArgumentException(nameof({pname}));");
                    }
                    else
                    {
                        WriteLine($"switch ({pname}.Kind)");
                        OpenBlock();
                        var kinds = field.Kinds.ToList();

                        //we need to check for Kind=None as well as node == null because that's what the red factory will pass
                        if (IsOptional(field))
                        {
                            kinds.Add(new Kind { Name = "None" });
                        }
                        foreach (var kind in kinds)
                        {
                            WriteLine($"case SyntaxKind.{kind.Name}:{(kind == kinds.Last() ? " break;" : "")}");
                        }

                        WriteLine($"default: throw new ArgumentException(nameof({pname}));");
                        CloseBlock();
                    }

                    if (IsOptional(field))
                    {
                        CloseBlock();
                    }
                }
            }

            WriteLine("#endif");

            if (nd.Name != "SkippedTokensTriviaSyntax" &&
                nd.Name != "DocumentationCommentTriviaSyntax" &&
                nd.Name != "IncompleteMemberSyntax" &&
                valueFields.Count + nodeFields.Count <= 3)
            {
                //int hash;
                //var cached = SyntaxNodeCache.TryGetNode((int)SyntaxKind.IdentifierName, identifier, this.context, out hash);
                //if (cached != null) return (IdentifierNameSyntax)cached;

                //var result = new IdentifierNameSyntax(SyntaxKind.IdentifierName, identifier, this.context);
                //if (hash >= 0)
                //{
                //    SyntaxNodeCache.AddNode(result, hash);
                //}

                //return result;

                WriteLine();
                //int hash;
                WriteLine("int hash;");
                //SyntaxNode cached = SyntaxNodeCache.TryGetNode(SyntaxKind.IdentifierName, identifier, this.context, out hash);
                if (withSyntaxFactoryContext)
                {
                    Write("var cached = CSharpSyntaxNodeCache.TryGetNode((int)");
                }
                else
                {
                    Write("var cached = SyntaxNodeCache.TryGetNode((int)");
                }

                WriteCtorArgList(nd, withSyntaxFactoryContext, valueFields, nodeFields);
                WriteLine(", out hash);");
                //    if (cached != null) return (IdentifierNameSyntax)cached;
                WriteLine($"if (cached != null) return ({nd.Name})cached;");
                WriteLine();

                //var result = new IdentifierNameSyntax(SyntaxKind.IdentifierName, identifier);
                Write($"var result = new {nd.Name}(");
                WriteCtorArgList(nd, withSyntaxFactoryContext, valueFields, nodeFields);
                WriteLine(");");
                //if (hash >= 0)
                WriteLine("if (hash >= 0)");
                //{
                OpenBlock();
                //    SyntaxNodeCache.AddNode(result, hash);
                WriteLine("SyntaxNodeCache.AddNode(result, hash);");
                //}
                CloseBlock();
                WriteLine();

                //return result;
                WriteLine("return result;");
            }
            else
            {
                WriteLine();
                Write($"return new {nd.Name}(");
                WriteCtorArgList(nd, withSyntaxFactoryContext, valueFields, nodeFields);
                WriteLine(");");
            }

            CloseBlock();
        }

        private void WriteGreenFactoryParameters(Node nd)
        {
            Write(CommaJoin(
                nd.Kinds.Count > 1 ? "SyntaxKind kind" : "",
                nd.Fields.Select(f =>
                {
                    var type = f.Type switch
                    {
                        "SyntaxNodeOrTokenList" => "Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList<CSharpSyntaxNode>",
                        _ when IsSeparatedNodeList(f.Type) || IsNodeList(f.Type) => $"Microsoft.CodeAnalysis.Syntax.InternalSyntax.{f.Type}",
                        _ => GetFieldType(f, green: true),
                    };

                    return $"{type} {CamelCase(f.Name)}";
                })));
        }

        private void WriteCtorArgList(Node nd, bool withSyntaxFactoryContext, List<Field> valueFields, List<Field> nodeFields)
        {
            Write(CommaJoin(
                nd.Kinds.Count == 1 ? $"SyntaxKind.{nd.Kinds[0].Name}" : "kind",
                nodeFields.Select(f =>
                    f.Type == "SyntaxList<SyntaxToken>" || IsAnyList(f.Type)
                        ? $"{CamelCase(f.Name)}.Node"
                        : CamelCase(f.Name)),
                // values are at end
                valueFields.Select(f => CamelCase(f.Name)),
                withSyntaxFactoryContext ? "this.context" : ""));
        }

        private void WriteRedTypes()
        {
            var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();
            foreach (var node in nodes)
            {
                WriteLine();
                this.WriteRedType(node);
            }
        }

        private List<Field> GetNodeOrNodeListFields(TreeType node)
            => node is AbstractNode an
                ? an.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList()
                : node is Node nd
                    ? nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList()
                    : new List<Field>();

        private void WriteRedType(TreeType node)
        {
            WriteComment(node.TypeComment, "");

            if (node is AbstractNode)
            {
                var nd = (AbstractNode)node;
                WriteLine($"public abstract partial class {node.Name} : {node.Base}");
                OpenBlock();
                WriteLine($"internal {node.Name}(InternalSyntax.CSharpSyntaxNode green, SyntaxNode? parent, int position)");
                WriteLine("  : base(green, parent, position)");
                OpenBlock();
                CloseBlock();

                var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
                var nodeFields = GetNodeOrNodeListFields(nd);

                foreach (var field in nodeFields)
                {
                    if (IsNodeOrNodeList(field.Type))
                    {
                        //red SyntaxLists can't contain tokens, so we switch to SyntaxTokenList
                        var fieldType = GetRedFieldType(field);
                        WriteLine();
                        WriteComment(field.PropertyComment, "");
                        WriteLine($"{"public"} abstract {(IsNew(field) ? "new " : "")}{fieldType} {field.Name} {{ get; }}");
                        WriteLine($"public {node.Name} With{field.Name}({fieldType} {CamelCase(field.Name)}) => With{field.Name}Core({CamelCase(field.Name)});");
                        WriteLine($"internal abstract {node.Name} With{field.Name}Core({fieldType} {CamelCase(field.Name)});");

                        if (IsAnyList(field.Type))
                        {
                            var argType = GetElementType(field.Type);
                            WriteLine();
                            WriteLine($"public {node.Name} Add{field.Name}(params {argType}[] items) => Add{field.Name}Core(items);");
                            WriteLine($"internal abstract {node.Name} Add{field.Name}Core(params {argType}[] items);");
                        }
                        else
                        {
                            var referencedNode = TryGetNodeForNestedList(field);
                            if (referencedNode != null)
                            {
                                foreach (var referencedNodeField in referencedNode.Fields)
                                {
                                    if (IsAnyList(referencedNodeField.Type))
                                    {
                                        var argType = GetElementType(referencedNodeField.Type);

                                        WriteLine();
                                        WriteLine($"public {node.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}(params {argType}[] items) => Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}Core(items);");
                                        WriteLine($"internal abstract {node.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}Core(params {argType}[] items);");
                                    }
                                }
                            }
                        }
                    }
                }

                foreach (var field in valueFields)
                {
                    WriteLine();
                    WriteComment(field.PropertyComment, "");
                    WriteLine($"{"public"} abstract {(IsNew(field) ? "new " : "")}{field.Type} {field.Name} {{ get; }}");
                }

                var baseType = GetTreeType(node.Base);
                if (baseType != null)
                {
                    var baseNodeFields = GetNodeOrNodeListFields(baseType);
                    if (baseNodeFields.Count > 0)
                    {
                        WriteLine();
                    }

                    foreach (var baseField in baseNodeFields)
                    {
                        WriteLine($"public new {node.Name} With{baseField.Name}({GetRedFieldType(baseField)} {CamelCase(baseField.Name)}) => ({node.Name})With{baseField.Name}Core({CamelCase(baseField.Name)});");
                    }

                    foreach (var baseField in baseNodeFields)
                    {
                        if (IsAnyList(baseField.Type))
                        {
                            var argType = GetElementType(baseField.Type);
                            WriteLine();
                            WriteLine($"public new {node.Name} Add{baseField.Name}(params {argType}[] items) => ({node.Name})Add{baseField.Name}Core(items);");
                        }
                        else
                        {
                            var referencedNode = TryGetNodeForNestedList(baseField);
                            if (referencedNode != null)
                            {
                                // look for list members...
                                foreach (var referencedNodeField in referencedNode.Fields)
                                {
                                    if (IsAnyList(referencedNodeField.Type))
                                    {
                                        var argType = GetElementType(referencedNodeField.Type);

                                        WriteLine();
                                        WriteLine($"public new {baseType.Name} Add{StripPost(baseField.Name, "Opt")}{referencedNodeField.Name}(params {argType}[] items) => Add{StripPost(baseField.Name, "Opt")}{referencedNodeField.Name}Core(items);");
                                    }
                                }
                            }
                        }
                    }
                }

                CloseBlock();
            }
            else if (node is Node)
            {
                var nd = (Node)node;
                WriteLine($"public sealed partial class {node.Name} : {node.Base}");
                OpenBlock();

                var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
                var nodeFields = nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

                foreach (var field in nodeFields)
                {
                    if (field.Type != "SyntaxToken"
                        && field.Type != "SyntaxList<SyntaxToken>")
                    {
                        if (IsSeparatedNodeList(field.Type) || field.Type == "SyntaxNodeOrTokenList")
                        {
                            WriteLine($"private SyntaxNode? {CamelCase(field.Name)};");
                        }
                        else
                        {
                            var type = GetFieldType(field, green: false);
                            WriteLine($"private {type} {CamelCase(field.Name)};");
                        }
                    }
                }

                // write constructor
                WriteLine();
                WriteLine($"internal {node.Name}(InternalSyntax.CSharpSyntaxNode green, SyntaxNode? parent, int position)");
                WriteLine("  : base(green, parent, position)");
                OpenBlock();
                CloseBlock();
                WriteLine();

                // property accessors
                for (int i = 0, n = nodeFields.Count; i < n; i++)
                {
                    var field = nodeFields[i];
                    if (field.Type == "SyntaxToken")
                    {
                        WriteComment(field.PropertyComment, "");
                        Write($"public {OverrideOrNewModifier(field)}{GetRedPropertyType(field)} {field.Name}");
                        if (IsOptional(field))
                        {
                            WriteLine();
                            OpenBlock();
                            WriteLine("get");
                            OpenBlock();
                            WriteLine($"var slot = ((Syntax.InternalSyntax.{node.Name})this.Green).{CamelCase(field.Name)};");
                            WriteLine($"return slot != null ? new SyntaxToken(this, slot, {GetChildPosition(i)}, {GetChildIndex(i)}) : default;"); CloseBlock();
                            CloseBlock();
                        }
                        else
                        {
                            WriteLine($" => new SyntaxToken(this, ((Syntax.InternalSyntax.{node.Name})this.Green).{CamelCase(field.Name)}, {GetChildPosition(i)}, {GetChildIndex(i)});");
                        }
                    }
                    else if (field.Type == "SyntaxList<SyntaxToken>")
                    {
                        WriteComment(field.PropertyComment, "");
                        WriteLine($"public {OverrideOrNewModifier(field)}SyntaxTokenList {field.Name}");
                        OpenBlock();
                        WriteLine("get");
                        OpenBlock();
                        WriteLine($"var slot = this.Green.GetSlot({i});");
                        WriteLine($"return slot != null ? new SyntaxTokenList(this, slot, {GetChildPosition(i)}, {GetChildIndex(i)}) : default;");
                        CloseBlock();
                        CloseBlock();
                    }
                    else
                    {
                        WriteComment(field.PropertyComment, "");
                        Write($"public {OverrideOrNewModifier(field)}{GetRedPropertyType(field)} {field.Name}");

                        if (IsNodeList(field.Type))
                        {
                            WriteLine($" => new {field.Type}(GetRed(ref this.{CamelCase(field.Name)}, {i}));");
                        }
                        else if (IsSeparatedNodeList(field.Type))
                        {
                            WriteLine();
                            OpenBlock();
                            WriteLine("get");
                            OpenBlock();

                            WriteLine($"var red = GetRed(ref this.{CamelCase(field.Name)}, {i});");
                            WriteLine($"return red != null ? new {field.Type}(red, {GetChildIndex(i)}) : default;");
                            CloseBlock();
                            CloseBlock();
                        }
                        else if (field.Type == "SyntaxNodeOrTokenList")
                        {
                            throw new InvalidOperationException("field cannot be a random SyntaxNodeOrTokenList");
                        }
                        else
                        {
                            var suffix = IsOptional(field) ? "" : "!";
                            if (i == 0)
                            {
                                WriteLine($" => GetRedAtZero(ref this.{CamelCase(field.Name)}){suffix};");
                            }
                            else
                            {
                                WriteLine($" => GetRed(ref this.{CamelCase(field.Name)}, {i}){suffix};");
                            }
                        }
                    }
                    WriteLine();
                }

                foreach (var field in valueFields)
                {
                    WriteComment(field.PropertyComment, "");
                    WriteLine($"{"public"} {OverrideOrNewModifier(field)}{field.Type} {field.Name} => ((Syntax.InternalSyntax.{node.Name})this.Green).{field.Name};");
                    WriteLine();
                }

                {
                    //GetNodeSlot forces creation of a red node.
                    Write("internal override SyntaxNode? GetNodeSlot(int index)");

                    var relevantNodes = nodeFields.Select((field, index) => (field, index))
                                                  .Where(t => t.field.Type != "SyntaxToken" && t.field.Type != "SyntaxList<SyntaxToken>");
                    if (!relevantNodes.Any())
                    {
                        WriteLine(" => null;");
                    }
                    else if (relevantNodes.Count() == 1)
                    {
                        var (field, index) = relevantNodes.Single();
                        var whenTrue = index == 0
                            ? $"GetRedAtZero(ref this.{CamelCase(field.Name)})"
                            : $"GetRed(ref this.{CamelCase(field.Name)}, {index})";

                        var suffix = IsOptional(field) ? "" : "!";
                        WriteLine($" => index == {index} ? {whenTrue}{suffix} : null;");
                    }
                    else
                    {
                        WriteLine();
                        Indent();
                        WriteLine("=> index switch");
                        OpenBlock();
                        foreach (var (field, index) in relevantNodes)
                        {
                            var suffix = IsOptional(field) ? "" : "!";
                            if (index == 0)
                            {
                                WriteLine($"{index} => GetRedAtZero(ref this.{CamelCase(field.Name)}){suffix},");
                            }
                            else
                            {
                                WriteLine($"{index} => GetRed(ref this.{CamelCase(field.Name)}, {index}){suffix},");
                            }
                        }
                        WriteLine("_ => null,");
                        CloseBlock(";");
                        Unindent();
                    }
                }

                WriteLine();

                {
                    //GetCachedSlot returns a red node if we have it.
                    Write("internal override SyntaxNode? GetCachedSlot(int index)");

                    var relevantNodes = nodeFields.Select((field, index) => (field, index))
                                                  .Where(t => t.field.Type != "SyntaxToken" && t.field.Type != "SyntaxList<SyntaxToken>");
                    if (!relevantNodes.Any())
                    {
                        WriteLine(" => null;");
                    }
                    else if (relevantNodes.Count() == 1)
                    {
                        var (field, index) = relevantNodes.Single();
                        WriteLine($" => index == {index} ? this.{CamelCase(field.Name)} : null;");
                    }
                    else
                    {
                        WriteLine();
                        Indent();
                        WriteLine("=> index switch");
                        OpenBlock();
                        foreach (var (field, index) in relevantNodes)
                        {
                            WriteLine($"{index} => this.{CamelCase(field.Name)},");
                        }
                        WriteLine("_ => null,");
                        CloseBlock(";");
                        Unindent();
                    }
                }

                this.WriteRedAcceptMethods(nd);
                this.WriteRedUpdateMethod(nd);
                this.WriteRedWithMethods(nd);
                this.WriteRedListHelperMethods(nd);

                CloseBlock();
            }
        }

        private string GetRedFieldType(Field field)
        {
            if (field.Type == "SyntaxList<SyntaxToken>")
                return "SyntaxTokenList";

            if (IsOptional(field) && IsNode(field.Type) && field.Type != "SyntaxToken")
                return field.Type + "?";

            return field.Type;
        }

        private string GetChildPosition(int i)
            => i == 0 ? "Position" : "GetChildPosition(" + i + ")";

        private string GetChildIndex(int i)
            => i == 0 ? "0" : "GetChildIndex(" + i + ")";

        private void WriteRedAcceptMethods(Node node)
        {
            WriteLine();
            WriteRedAcceptMethod(node, false);
            WriteRedAcceptMethod(node, true);
        }

        private void WriteRedAcceptMethod(Node node, bool genericResult)
        {
            string genericArgs = genericResult ? "<TResult>" : "";
            WriteLine($"public override {(genericResult ? "TResult" : "void")} Accept{genericArgs}(CSharpSyntaxVisitor{genericArgs} visitor) => visitor.Visit{StripPost(node.Name, "Syntax")}(this);");
        }

        private void WriteRedVisitors()
        {
            WriteRedVisitor(genericResult: true);
            WriteRedVisitor(genericResult: false);
        }

        private void WriteRedVisitor(bool genericResult)
        {
            string genericArgs = genericResult ? "<TResult>" : "";
            var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();

            WriteLine();
            WriteLine("public partial class CSharpSyntaxVisitor" + genericArgs);
            OpenBlock();
            int nWritten = 0;
            foreach (var node in nodes.OfType<Node>())
            {
                if (nWritten > 0)
                    WriteLine();
                nWritten++;
                WriteComment($"<summary>Called when the visitor visits a {node.Name} node.</summary>");
                WriteLine($"public virtual {(genericResult ? "TResult" : "void")} Visit{StripPost(node.Name, "Syntax")}({node.Name} node) => this.DefaultVisit(node);");
            }
            CloseBlock();
        }

        private void WriteRedUpdateMethod(Node node)
        {
            WriteLine();
            Write($"public {node.Name} Update(");
            Write(CommaJoin(
                node.Fields.Select(f => $"{GetRedPropertyType(f)} {CamelCase(f.Name)}")));
            WriteLine(")");
            OpenBlock();

            Write("if (");
            int nCompared = 0;
            foreach (var field in node.Fields)
            {
                if (IsDerivedOrListOfDerived("SyntaxNode", field.Type) || IsDerivedOrListOfDerived("SyntaxToken", field.Type) || field.Type == "SyntaxNodeOrTokenList")
                {
                    if (nCompared > 0)
                        Write(" || ");
                    Write($"{CamelCase(field.Name)} != this.{field.Name}");
                    nCompared++;
                }
            }
            if (nCompared > 0)
            {
                WriteLine(")");
                OpenBlock();
                Write($"var newNode = SyntaxFactory.{StripPost(node.Name, "Syntax")}(");
                Write(CommaJoin(
                    node.Kinds.Count > 1 ? "this.Kind()" : "",
                    node.Fields.Select(f => CamelCase(f.Name))));
                WriteLine(");");
                WriteLine("var annotations = GetAnnotations();");
                WriteLine("return annotations?.Length > 0 ? newNode.WithAnnotations(annotations) : newNode;");
                CloseBlock();
            }

            WriteLine();
            WriteLine("return this;");
            CloseBlock();
        }

        private void WriteRedWithMethods(Node node)
        {
            foreach (var field in node.Fields)
            {
                var type = this.GetRedPropertyType(field);

                if (field == node.Fields.First())
                {
                    WriteLine();
                }

                var isNew = false;
                if (IsOverride(field))
                {
                    var (baseType, baseField) = GetHighestBaseTypeWithField(node, field.Name);
                    if (baseType != null)
                    {
                        Write($"internal override {baseType.Name} With{field.Name}Core({GetRedPropertyType(baseField)} {CamelCase(field.Name)}) => With{field.Name}({CamelCase(field.Name)}");
                        if (IsOptional(baseField) && !IsOptional(field))
                        {
                            Write($" ?? throw new ArgumentNullException(nameof({CamelCase(field.Name)}))");
                        }
                        WriteLine(");");

                        isNew = true;
                    }
                }

                Write(
                    $"public{(isNew ? " new " : " ")}{node.Name} With{StripPost(field.Name, "Opt")}({type} {CamelCase(field.Name)})" +
                    " => Update(");

                // call update inside each setter
                Write(CommaJoin(node.Fields.Select(f =>
                    f == field ? CamelCase(f.Name) : $"this.{f.Name}")));
                WriteLine(");");
            }
        }

        private (TreeType type, Field field) GetHighestBaseTypeWithField(TreeType node, string name)
        {
            TreeType bestType = null;
            Field bestField = null;
            for (var current = node; current != null; current = TryGetBaseType(current))
            {
                var fields = GetNodeOrNodeListFields(current);
                var field = fields.FirstOrDefault(f => f.Name == name);
                if (field != null)
                {
                    bestType = current;
                    bestField = field;
                }
            }

            return (bestType, bestField);
        }

        private TreeType TryGetBaseType(TreeType node)
            => node is AbstractNode an
                ? GetTreeType(an.Base)
                : node is Node n
                    ? GetTreeType(n.Base)
                    : null;

        private void WriteRedListHelperMethods(Node node)
        {
            var wroteNewLine = false;
            foreach (var field in node.Fields)
            {
                if (IsAnyList(field.Type))
                {
                    if (!wroteNewLine)
                    {
                        WriteLine();
                        wroteNewLine = true;
                    }
                    // write list helper methods for list properties
                    WriteRedListHelperMethods(node, field);
                }
                else
                {
                    var referencedNode = TryGetNodeForNestedList(field);
                    if (referencedNode != null)
                    {
                        // look for list members...
                        foreach (var referencedNodeField in referencedNode.Fields)
                        {
                            if (IsAnyList(referencedNodeField.Type))
                            {
                                if (!wroteNewLine)
                                {
                                    WriteLine();
                                    wroteNewLine = true;
                                }
                                WriteRedNestedListHelperMethods(node, field, referencedNode, referencedNodeField);
                            }
                        }
                    }
                }
            }
        }

        private Node TryGetNodeForNestedList(Field field)
        {
            Node referencedNode = GetNode(field.Type);
            if (referencedNode != null && (!IsOptional(field) || RequiredFactoryArgumentCount(referencedNode) == 0))
            {
                return referencedNode;
            }

            return null;
        }

        private void WriteRedListHelperMethods(Node node, Field field)
        {
            var argType = GetElementType(field.Type);

            var isNew = false;
            if (IsOverride(field))
            {
                var (baseType, baseField) = GetHighestBaseTypeWithField(node, field.Name);
                if (baseType != null)
                {
                    var baseArgType = GetElementType(baseField.Type);
                    WriteLine($"internal override {baseType.Name} Add{field.Name}Core(params {baseArgType}[] items) => Add{field.Name}(items);");
                    isNew = true;
                }
            }

            WriteLine($"public{(isNew ? " new " : " ")}{node.Name} Add{field.Name}(params {argType}[] items) => With{StripPost(field.Name, "Opt")}(this.{field.Name}.AddRange(items));");
        }

        private void WriteRedNestedListHelperMethods(Node node, Field field, Node referencedNode, Field referencedNodeField)
        {
            var argType = GetElementType(referencedNodeField.Type);

            var isNew = false;
            if (IsOverride(field))
            {
                var (baseType, _) = GetHighestBaseTypeWithField(node, field.Name);
                if (baseType != null)
                {
                    WriteLine($"internal override {baseType.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}Core(params {argType}[] items) => Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}(items);");
                    isNew = true;
                }
            }

            // AddBaseListTypes
            Write($"public{(isNew ? " new " : " ")}{node.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}(params {argType}[] items)");

            if (IsOptional(field))
            {
                WriteLine();
                OpenBlock();
                var factoryName = StripPost(referencedNode.Name, "Syntax");
                var varName = StripPost(CamelCase(field.Name), "Opt");
                WriteLine($"var {varName} = this.{field.Name} ?? SyntaxFactory.{factoryName}();");
                WriteLine($"return With{StripPost(field.Name, "Opt")}({varName}.With{StripPost(referencedNodeField.Name, "Opt")}({varName}.{referencedNodeField.Name}.AddRange(items)));");
                CloseBlock();
            }
            else
            {
                WriteLine($" => With{StripPost(field.Name, "Opt")}(this.{field.Name}.With{StripPost(referencedNodeField.Name, "Opt")}(this.{field.Name}.{referencedNodeField.Name}.AddRange(items)));");
            }
        }

        private void WriteRedRewriter()
        {
            var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();

            WriteLine();
            WriteLine("public partial class CSharpSyntaxRewriter : CSharpSyntaxVisitor<SyntaxNode?>");
            OpenBlock();

            int nWritten = 0;
            foreach (var node in nodes.OfType<Node>())
            {
                if (nWritten > 0)
                    WriteLine();
                nWritten++;
                WriteLine($"public override SyntaxNode? Visit{StripPost(node.Name, "Syntax")}({node.Name} node)");

                if (node.Fields.Count == 0)
                {
                    WriteLine("    => node;");
                }
                else
                {
                    Write("    => node.Update(");
                    Write(CommaJoin(node.Fields.Select(f =>
                    {
                        if (IsNodeOrNodeList(f.Type))
                        {
                            if (IsAnyList(f.Type))
                                return $"VisitList(node.{f.Name})";
                            else if (f.Type == "SyntaxToken")
                                return $"VisitToken(node.{f.Name})";
                            else if (IsOptional(f))
                                return $"({(GetFieldType(f, green: false))})Visit(node.{f.Name})";
                            else
                                return $"({(GetFieldType(f, green: false))})Visit(node.{f.Name}) ?? throw new ArgumentNullException(\"{CamelCase(f.Name)}\")";
                        }

                        return $"node.{f.Name}";
                    })));

                    WriteLine(");");
                }
            }
            CloseBlock();
        }

        private void WriteRedFactories()
        {
            var nodes = Tree.Types.Where(n => !(n is PredefinedNode) && !(n is AbstractNode)).OfType<Node>().ToList();
            WriteLine();
            WriteLine("public static partial class SyntaxFactory");
            OpenBlock();

            foreach (var node in nodes)
            {
                this.WriteRedFactory(node);
                bool skipConvenienceFactories = node.SkipConvenienceFactories != null && string.Compare(node.SkipConvenienceFactories, "true", true) == 0;
                if (!skipConvenienceFactories)
                {
                    this.WriteRedFactoryWithNoAutoCreatableTokens(node);
                    this.WriteRedMinimalFactory(node);
                    this.WriteRedMinimalFactory(node, withStringNames: true);
                }
                this.WriteKindConverters(node);
            }

            CloseBlock();
        }

        protected bool CanBeAutoCreated(Node node, Field field)
            => IsAutoCreatableToken(node, field) || IsAutoCreatableNode(field);

        private bool IsAutoCreatableToken(Node node, Field field)
        {
            return field.Type == "SyntaxToken"
                && field.Kinds != null
                && ((field.Kinds.Count == 1 && field.Kinds[0].Name != "IdentifierToken" && !field.Kinds[0].Name.EndsWith("LiteralToken", StringComparison.Ordinal)) || (field.Kinds.Count > 1 && field.Kinds.Count == node.Kinds.Count));
        }

        private bool IsAutoCreatableNode(Field field)
        {
            var referencedNode = GetNode(field.Type);
            return (referencedNode != null && RequiredFactoryArgumentCount(referencedNode) == 0);
        }

        private bool IsRequiredFactoryField(Node node, Field field)
        {
            return (!IsOptional(field) && !IsAnyList(field.Type) && !CanBeAutoCreated(node, field)) || IsValueField(field);
        }

        private bool IsValueField(Field field)
        {
            return !IsNodeOrNodeList(field.Type);
        }

        private int RequiredFactoryArgumentCount(Node nd, bool includeKind = true)
        {
            int count = 0;

            // kind must be specified in factory
            if (nd.Kinds.Count > 1 && includeKind)
            {
                count++;
            }

            foreach (var field in nd.Fields)
            {
                if (IsRequiredFactoryField(nd, field))
                {
                    count++;
                }
            }

            return count;
        }

        private int OptionalFactoryArgumentCount(Node nd)
        {
            int count = 0;
            foreach (var field in nd.Fields)
            {
                if (IsOptional(field) || CanBeAutoCreated(nd, field) || IsAnyList(field.Type))
                {
                    count++;
                }
            }

            return count;
        }

        // full factory signature with nothing optional
        private void WriteRedFactory(Node nd)
        {
            this.WriteLine();

            var valueFields = nd.Fields.Where(n => IsValueField(n)).ToList();
            var nodeFields = nd.Fields.Where(n => !IsValueField(n)).ToList();

            WriteComment($"<summary>Creates a new {nd.Name} instance.</summary>");

            Write($"public static {nd.Name} {StripPost(nd.Name, "Syntax")}(");
            WriteRedFactoryParameters(nd);

            WriteLine(")");
            OpenBlock();

            // validate kinds
            if (nd.Kinds.Count >= 2)
            {
                WriteLine("switch (kind)");
                OpenBlock();
                foreach (var kind in nd.Kinds)
                {
                    WriteLine($"case SyntaxKind.{kind.Name}:{(kind == nd.Kinds.Last() ? " break;" : "")}");
                }
                WriteLine("default: throw new ArgumentException(nameof(kind));");
                CloseBlock();
            }

            // validate parameters
            foreach (var field in nodeFields)
            {
                var pname = CamelCase(field.Name);

                if (field.Type == "SyntaxToken")
                {
                    if (field.Kinds != null && field.Kinds.Count > 0)
                    {
                        var kinds = field.Kinds.ToList();
                        if (IsOptional(field))
                        {
                            kinds.Add(new Kind { Name = "None" });
                        }

                        if (kinds.Count == 1)
                        {
                            WriteLine($"if ({pname}.Kind() != SyntaxKind.{kinds[0].Name}) throw new ArgumentException(nameof({pname}));");
                        }
                        else
                        {
                            WriteLine($"switch ({pname}.Kind())");
                            OpenBlock();
                            foreach (var kind in kinds)
                            {
                                WriteLine($"case SyntaxKind.{kind.Name}:{(kind == kinds.Last() ? " break;" : "")}");
                            }
                            WriteLine($"default: throw new ArgumentException(nameof({pname}));");
                            CloseBlock();
                        }
                    }
                }
                else if (!IsAnyList(field.Type) && !IsOptional(field))
                {
                    WriteLine($"if ({CamelCase(field.Name)} == null) throw new ArgumentNullException(nameof({CamelCase(field.Name)}));");
                }
            }

            Write($"return ({nd.Name})Syntax.InternalSyntax.SyntaxFactory.{StripPost(nd.Name, "Syntax")}(");
            Write(CommaJoin(
                nd.Kinds.Count > 1 ? "kind" : "",
                nodeFields.Select(f =>
                {
                    if (f.Type == "SyntaxToken")
                    {
                        if (IsOptional(f))
                            return $"(Syntax.InternalSyntax.SyntaxToken?){CamelCase(f.Name)}.Node";
                        else
                            // We know the GreenNode is not null because it gets a type check earlier in the generated method
                            return $"(Syntax.InternalSyntax.SyntaxToken){CamelCase(f.Name)}.Node!";
                    }
                    else if (f.Type == "SyntaxList<SyntaxToken>")
                        return $"{CamelCase(f.Name)}.Node.ToGreenList<Syntax.InternalSyntax.SyntaxToken>()";
                    else if (IsNodeList(f.Type))
                        return $"{CamelCase(f.Name)}.Node.ToGreenList<Syntax.InternalSyntax.{GetElementType(f.Type)}>()";
                    else if (IsSeparatedNodeList(f.Type))
                        return $"{CamelCase(f.Name)}.Node.ToGreenSeparatedList<Syntax.InternalSyntax.{GetElementType(f.Type)}>()";
                    else if (f.Type == "SyntaxNodeOrTokenList")
                        return $"{CamelCase(f.Name)}.Node.ToGreenList<Syntax.InternalSyntax.CSharpSyntaxNode>()";
                    else if (IsOptional(f))
                        return $"{CamelCase(f.Name)} == null ? null : (Syntax.InternalSyntax.{f.Type}){CamelCase(f.Name)}.Green";
                    else
                        return $"(Syntax.InternalSyntax.{f.Type}){CamelCase(f.Name)}.Green";
                }),
                // values are at end
                valueFields.Select(f => CamelCase(f.Name))));

            WriteLine(").CreateRed();");
            CloseBlock();
        }

        private void WriteRedFactoryParameters(Node nd)
        {
            Write(CommaJoin(
                nd.Kinds.Count > 1 ? "SyntaxKind kind" : "",
                nd.Fields.Select(f => $"{this.GetRedPropertyType(f)} {CamelCase(f.Name)}")));
        }

        private string GetRedPropertyType(Field field)
        {
            if (field.Type == "SyntaxList<SyntaxToken>")
                return "SyntaxTokenList";
            if (IsOptional(field) && IsNode(field.Type) && field.Type != "SyntaxToken")
                return field.Type + "?";
            return field.Type;
        }

        private string GetDefaultValue(Node nd, Field field)
        {
            System.Diagnostics.Debug.Assert(!IsRequiredFactoryField(nd, field));

            if (IsOptional(field) || IsAnyList(field.Type))
            {
                var type = GetRedPropertyType(field);
                return type == "SyntaxTokenList" ? "default(SyntaxTokenList)" : "default";
            }
            else if (field.Type == "SyntaxToken")
            {
                // auto construct token?
                if (field.Kinds.Count == 1)
                {
                    return $"SyntaxFactory.Token(SyntaxKind.{field.Kinds[0].Name})";
                }
                else
                {
                    return $"SyntaxFactory.Token(Get{StripPost(nd.Name, "Syntax")}{StripPost(field.Name, "Opt")}Kind(kind))";
                }
            }
            else
            {
                var referencedNode = GetNode(field.Type);
                return $"SyntaxFactory.{StripPost(referencedNode.Name, "Syntax")}()";
            }
        }

        // Writes Get<Property>Kind() methods for converting between node kind and member token kinds...
        private void WriteKindConverters(Node nd)
        {
            foreach (var field in nd.Fields)
            {
                if (field.Type == "SyntaxToken" && CanBeAutoCreated(nd, field) && field.Kinds.Count > 1)
                {
                    WriteLine();
                    WriteLine($"private static SyntaxKind Get{StripPost(nd.Name, "Syntax")}{StripPost(field.Name, "Opt")}Kind(SyntaxKind kind)");
                    Indent();
                    WriteLine("=> kind switch");
                    OpenBlock();

                    for (int k = 0; k < field.Kinds.Count; k++)
                    {
                        var nKind = nd.Kinds[k];
                        var pKind = field.Kinds[k];
                        WriteLine($"SyntaxKind.{nKind.Name} => SyntaxKind.{pKind.Name},");
                    }

                    WriteLine("_ => throw new ArgumentOutOfRangeException(),");
                    CloseBlock(";");
                    Unindent();
                }
            }
        }

        private IEnumerable<Field> DetermineRedFactoryWithNoAutoCreatableTokenFields(Node nd)
        {
            return nd.Fields.Where(f => !IsAutoCreatableToken(nd, f));
        }

        // creates a factory without auto-creatable token arguments
        private void WriteRedFactoryWithNoAutoCreatableTokens(Node nd)
        {
            var nAutoCreatableTokens = nd.Fields.Count(f => IsAutoCreatableToken(nd, f));
            if (nAutoCreatableTokens == 0)
                return; // already handled by general factory

            var factoryWithNoAutoCreatableTokenFields = new HashSet<Field>(DetermineRedFactoryWithNoAutoCreatableTokenFields(nd));
            var minimalFactoryFields = DetermineMinimalFactoryFields(nd);
            if (minimalFactoryFields != null && factoryWithNoAutoCreatableTokenFields.SetEquals(minimalFactoryFields))
            {
                return; // will be handled in minimal factory case
            }

            this.WriteLine();

            WriteComment($"<summary>Creates a new {nd.Name} instance.</summary>");
            Write($"public static {nd.Name} {StripPost(nd.Name, "Syntax")}(");
            Write(CommaJoin(
                nd.Kinds.Count > 1 ? "SyntaxKind kind" : "",
                nd.Fields.Where(factoryWithNoAutoCreatableTokenFields.Contains).Select(
                    f => $"{GetRedPropertyType(f)} {CamelCase(f.Name)}")));
            WriteLine(")");

            Write($"    => SyntaxFactory.{StripPost(nd.Name, "Syntax")}(");
            Write(CommaJoin(
                nd.Kinds.Count > 1 ? "kind" : "",
                nd.Fields.Select(f => factoryWithNoAutoCreatableTokenFields.Contains(f)
                    ? CamelCase(f.Name)
                    : GetDefaultValue(nd, f))));

            WriteLine(");");
        }

        private Field DetermineMinimalOptionalField(Node nd)
        {
            // first if there is a single list, then choose the list because it would not have been optional
            int listCount = nd.Fields.Count(f => IsAnyNodeList(f.Type));
            if (listCount == 1)
            {
                return nd.Fields.First(f => IsAnyNodeList(f.Type));
            }
            else
            {
                // otherwise, if there is a single optional node, use that..
                int nodeCount = nd.Fields.Count(f => IsNode(f.Type) && f.Type != "SyntaxToken");
                if (nodeCount == 1)
                {
                    return nd.Fields.First(f => IsNode(f.Type) && f.Type != "SyntaxToken");
                }
                else
                {
                    return null;
                }
            }
        }

        private IEnumerable<Field> DetermineMinimalFactoryFields(Node nd)
        {
            // special case to allow a single optional argument if there would have been no arguments
            // and we can determine a best single argument.
            Field allowOptionalField = null;

            var optionalCount = OptionalFactoryArgumentCount(nd);
            if (optionalCount == 0)
            {
                return null; // no fields...
            }

            var requiredCount = RequiredFactoryArgumentCount(nd, includeKind: false);
            if (requiredCount == 0 && optionalCount > 1)
            {
                allowOptionalField = DetermineMinimalOptionalField(nd);
            }

            return nd.Fields.Where(f => IsRequiredFactoryField(nd, f) || allowOptionalField == f);
        }

        // creates a factory with only the required arguments (everything else is defaulted)
        private void WriteRedMinimalFactory(Node nd, bool withStringNames = false)
        {
            var optionalCount = OptionalFactoryArgumentCount(nd);
            if (optionalCount == 0)
                return; // already handled w/ general factory method

            var minimalFactoryfields = new HashSet<Field>(DetermineMinimalFactoryFields(nd));

            if (withStringNames && minimalFactoryfields.Count(f => IsRequiredFactoryField(nd, f) && CanAutoConvertFromString(f)) == 0)
                return; // no string-name overload necessary

            this.WriteLine();

            WriteComment($"<summary>Creates a new {nd.Name} instance.</summary>");
            Write($"public static {nd.Name} {StripPost(nd.Name, "Syntax")}(");
            Write(CommaJoin(
                nd.Kinds.Count > 1 ? "SyntaxKind kind" : "",
                nd.Fields.Where(minimalFactoryfields.Contains).Select(f =>
                {
                    var type = GetRedPropertyType(f);

                    if (IsRequiredFactoryField(nd, f))
                    {
                        if (withStringNames && CanAutoConvertFromString(f))
                            type = "string";

                        return $"{type} {CamelCase(f.Name)}";
                    }
                    else
                    {
                        if (IsNode(f.Type) && !IsOptional(f) && f.Type != "SyntaxToken")
                            type += "?";

                        return $"{type} {CamelCase(f.Name)} = default";
                    }
                })));
            WriteLine(")");

            Write($"    => SyntaxFactory.{StripPost(nd.Name, "Syntax")}(");

            Write(CommaJoin(
                nd.Kinds.Count > 1 ? "kind" : "",
                nd.Fields.Select(f =>
                {
                    if (minimalFactoryfields.Contains(f))
                    {
                        if (IsRequiredFactoryField(nd, f))
                        {
                            if (withStringNames && CanAutoConvertFromString(f))
                                return $"{GetStringConverterMethod(f)}({CamelCase(f.Name)})";
                            else
                                return CamelCase(f.Name);
                        }
                        else
                        {
                            if (IsOptional(f) || IsAnyList(f.Type))
                                return CamelCase(f.Name);
                            else
                                return $"{CamelCase(f.Name)} ?? {GetDefaultValue(nd, f)}";
                        }
                    }

                    return GetDefaultValue(nd, f);
                })));

            WriteLine(");");
        }

        private bool CanAutoConvertFromString(Field field)
        {
            return IsIdentifierToken(field) || IsIdentifierNameSyntax(field);
        }

        private bool IsIdentifierToken(Field field)
        {
            return field.Type == "SyntaxToken" && field.Kinds != null && field.Kinds.Count == 1 && field.Kinds[0].Name == "IdentifierToken";
        }

        private bool IsIdentifierNameSyntax(Field field)
        {
            return field.Type == "IdentifierNameSyntax";
        }

        private string GetStringConverterMethod(Field field)
        {
            if (IsIdentifierToken(field))
            {
                return "SyntaxFactory.Identifier";
            }
            else if (IsIdentifierNameSyntax(field))
            {
                return "SyntaxFactory.IdentifierName";
            }
            else
            {
                throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Anything inside a &lt;Comment&gt; tag gets written out (escaping untouched) as the
        /// XML doc comment.  Line breaks will be preserved.
        /// </summary>
        private void WriteComment(string comment)
        {
            if (comment != null)
            {
                var lines = comment.Split(new string[] { "\r", "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
                foreach (var line in lines.Where(l => !string.IsNullOrWhiteSpace(l)))
                {
                    WriteLine($"/// {line.TrimStart()}");
                }
            }
        }

        /// <summary>
        /// Anything inside a &lt;Comment&gt; tag gets written out (escaping untouched) as the
        /// XML doc comment.  Line breaks will be preserved.
        /// </summary>
        private void WriteComment(Comment comment, string indent)
        {
            if (comment != null)
            {
                foreach (XmlElement element in comment.Body)
                {
                    string[] lines = element.OuterXml.Split(new string[] { "\r", "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (string line in lines.Where(l => !string.IsNullOrWhiteSpace(l)))
                    {
                        WriteLine($"{indent}/// {line.TrimStart()}");
                    }
                }
            }
        }
    }
}
